package app.caihan.scframe.util

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.concurrent.atomic.AtomicReference

/**
 * 在 Android 开发中使用协程 | 代码实战
 * https://juejin.im/post/6844904150694625288
 * 解决并发模式下的协程问题
 *
 * 1.在启动更多协程之前取消之前的任务
 * 2.让下一个任务排队等待前一个任务执行完成
 * 3.如果有一个任务正在执行，返回该任务，而不是启动一个新的任务。
 *
 * @author Seven-C
 * @date 2020/8/20
 */


/**
 * 方案 1: 取消之前的任务
 * 对于排序和过滤的情况，新请求进来，取消上一个，这样的方案是很适合的
 *
 * 方案 3: 复用前一个任务
 */
class ControlledRunner<T> {

    private val activeTask = AtomicReference<Deferred<T>?>(null)

    /**
     * 取消之前的任务
     * class ProductsRepository(val productsDao: ProductsDao, val productsApi: ProductsService) {
     *      var controlledRunner = ControlledRunner<List<ProductListing>>()
     *
     *      suspend fun loadSortedProducts(ascending: Boolean): List<ProductListing> {
     *          // 在开启新的排序之前，先取消上一个排序任务
     *          return controlledRunner.cancelPreviousThenRun {
     *              if (ascending) {
     *                  productsDao.loadProductsByDateStockedAscending()
     *              } else {
     *                  productsDao.loadProductsByDateStockedDescending()
     *              }
     *           }
     *       }
     *   }
     *
     * */
    suspend fun cancelPreviousThenRun(block: suspend () -> T): T {
        activeTask.get()?.cancelAndJoin()
        return coroutineScope {
            val newTask = async(start = CoroutineStart.LAZY) {
                block()
            }
            newTask.invokeOnCompletion {
                activeTask.compareAndSet(newTask, null)
            }

            val result: T
            while (true) {
                if (!activeTask.compareAndSet(null, newTask)) {
                    activeTask.get()?.cancelAndJoin()
                    yield()
                } else {
                    result = newTask.await()
                    break
                }
            }
            result
        }
    }

    /**
     * 方案 3: 复用前一个任务
     * class ProductsRepository(val productsDao: ProductsDao, val productsApi: ProductsService) {
     *      var controlledRunner = ControlledRunner<List<ProductListing>>()
     *
     *      suspend fun fetchProductsFromBackend(): List<ProductListing> {
     *           //如如果已经有一个正在运行的请求，那么就返回它。如果没有的话，开启一个新的请求。
     *          return controlledRunner.joinPreviousOrRun {
     *              val result = productsApi.getProducts()
     *              productsDao.insertAll(result)
     *              result
     *           }
     *       }
     *   }
     *
     * */
    suspend fun joinPreviousOrRun(block: suspend () -> T): T {
        //如果存在 activeTask，直接返回它的结果，并不会执行代码块
        activeTask.get()?.let {
            return it.await()
        }
        return coroutineScope {
            val newTask = async(start = CoroutineStart.LAZY) {
                block()
            }
            newTask.invokeOnCompletion {
                activeTask.compareAndSet(newTask, null)
            }
            val result: T
            while (true) {
                if (!activeTask.compareAndSet(null, newTask)) {
                    val currentTask = activeTask.get()
                    if (currentTask != null) {
                        newTask.cancel()
                        result = currentTask.await()
                        break
                    } else {
                        yield()
                    }
                } else {
                    result = newTask.await()
                    break
                }
            }
            result
        }
    }
}

/**
 * 方案 2: 使用互斥锁
 * 注意: 这个方法对于排序或者是过滤来说并不是一个很好的解决方案，但是它对于解决网络请求引起的并发问题非常适合。
 */
class SingleRunner {

    private val mutex = Mutex()

    /**
     * 将下一个任务入队
     * class ProductsRepository(val productsDao: ProductsDao, val productsApi: ProductsService) {
     *      var singleRunner = SingleRunner<>()
     *
     *      suspend fun loadSortedProducts(ascending: Boolean): List<ProductListing> {
     *          // 开始新的任务之前，等待之前的排序任务完成
     *          return singleRunner.afterPrevious {
     *              if (ascending) {
     *                  productsDao.loadProductsByDateStockedAscending()
     *              } else {
     *                  productsDao.loadProductsByDateStockedDescending()
     *              }
     *           }
     *       }
     *   }
     *
     * */
    suspend fun <T> afterPrevious(block: suspend () -> T): T {
        mutex.withLock {
            return block()
        }
    }
}